summaryrefslogtreecommitdiff
path: root/app/[lng]/evcp/(evcp)/bid/page.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'app/[lng]/evcp/(evcp)/bid/page.tsx')
-rw-r--r--app/[lng]/evcp/(evcp)/bid/page.tsx111
1 files changed, 111 insertions, 0 deletions
diff --git a/app/[lng]/evcp/(evcp)/bid/page.tsx b/app/[lng]/evcp/(evcp)/bid/page.tsx
new file mode 100644
index 00000000..7480ce88
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/bid/page.tsx
@@ -0,0 +1,111 @@
+import { Suspense } from "react"
+import { Shell } from "@/components/shell"
+import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
+import {
+ getBiddings,
+ getBiddingStatusCounts,
+ getBiddingTypeCounts,
+ getBiddingManagerCounts,
+ getBiddingMonthlyStats
+} from "@/lib/bidding/service"
+import { searchParamsCache } from "@/lib/bidding/validation"
+import { BiddingsPageHeader } from "@/lib/bidding/list/biddings-page-header"
+import { BiddingsStatsCards } from "@/lib/bidding/list/biddings-stats-cards"
+import { BiddingsTable } from "@/lib/bidding/list/biddings-table"
+
+export const metadata = {
+ title: "입찰 목록",
+ description: "입찰 공고를 생성하고 진행 상황을 관리할 수 있습니다.",
+}
+
+interface BiddingsPageProps {
+ searchParams: Record<string, string | string[] | undefined>
+}
+
+export default async function BiddingsPage({ searchParams }: BiddingsPageProps) {
+ // ✅ nuqs searchParamsCache로 파싱 (타입 안전성 보장)
+ const search = searchParamsCache.parse(searchParams)
+
+ // ✅ 모든 데이터를 병렬로 로드
+ const promises = Promise.all([
+ getBiddings(search),
+ getBiddingStatusCounts(),
+ getBiddingTypeCounts(),
+ getBiddingManagerCounts(),
+ getBiddingMonthlyStats(),
+ ])
+
+ return (
+ <Shell className="gap-4">
+ {/* ═══════════════════════════════════════════════════════════════ */}
+ {/* 페이지 헤더 */}
+ {/* ═══════════════════════════════════════════════════════════════ */}
+ <BiddingsPageHeader />
+
+ {/* ═══════════════════════════════════════════════════════════════ */}
+ {/* 통계 카드들 */}
+ {/* ═══════════════════════════════════════════════════════════════ */}
+ <Suspense fallback={<BiddingsStatsCardsSkeleton />}>
+ <BiddingsStatsCardsWrapper promises={promises} />
+ </Suspense>
+
+ {/* ═══════════════════════════════════════════════════════════════ */}
+ {/* 메인 테이블 */}
+ {/* ═══════════════════════════════════════════════════════════════ */}
+ <Suspense
+ fallback={
+ <DataTableSkeleton
+ columnCount={20}
+ searchableColumnCount={3}
+ filterableColumnCount={4}
+ cellWidths={["10rem", "8rem", "12rem", "15rem", "10rem", "8rem"]}
+ shrinkZero
+ />
+ }
+ >
+ <BiddingsTable promises={promises} />
+ </Suspense>
+ </Shell>
+ )
+}
+
+// ═══════════════════════════════════════════════════════════════
+// 통계 카드 래퍼 컴포넌트
+// ═══════════════════════════════════════════════════════════════
+async function BiddingsStatsCardsWrapper({
+ promises
+}: {
+ promises: Promise<[
+ Awaited<ReturnType<typeof getBiddings>>,
+ Awaited<ReturnType<typeof getBiddingStatusCounts>>,
+ Awaited<ReturnType<typeof getBiddingTypeCounts>>,
+ Awaited<ReturnType<typeof getBiddingManagerCounts>>,
+ Awaited<ReturnType<typeof getBiddingMonthlyStats>>
+ ]>
+}) {
+ const [biddingsResult, statusCounts, typeCounts, managerCounts, monthlyStats] = await promises
+
+ return (
+ <BiddingsStatsCards
+ total={biddingsResult.total}
+ statusCounts={statusCounts}
+ typeCounts={typeCounts}
+ managerCounts={managerCounts}
+ monthlyStats={monthlyStats}
+ />
+ )
+}
+
+// 통계 카드 스켈레톤
+function BiddingsStatsCardsSkeleton() {
+ return (
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
+ {Array.from({ length: 4 }).map((_, i) => (
+ <div key={i} className="rounded-lg border p-6">
+ <div className="h-4 bg-muted rounded animate-pulse mb-2" />
+ <div className="h-8 bg-muted rounded animate-pulse" />
+ </div>
+ ))}
+ </div>
+ )
+} \ No newline at end of file